/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2006-2008 Sun Microsystems, Inc.
 * Portions Copyright 2014-2016 ForgeRock AS.
 */
package org.opends.server.core;

import static org.opends.messages.ConfigMessages.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.ClassPropertyDefinition;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.config.server.ConfigurationChangeListener;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.server.config.meta.AccessControlHandlerCfgDefn;
import org.forgerock.opendj.server.config.server.AccessControlHandlerCfg;
import org.forgerock.util.Utils;
import org.opends.server.api.AccessControlHandler;
import org.opends.server.api.AlertGenerator;
import org.opends.server.types.InitializationException;

/**
 * This class manages the application-wide access-control configuration.
 * <p>
 * When access control is disabled a default "permissive" access control
 * implementation is used, which permits all operations regardless of the
 * identity of the user.
 */
public final class AccessControlConfigManager
       implements AlertGenerator ,
                  ConfigurationChangeListener<AccessControlHandlerCfg>
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  private static final String CLASS_NAME =
    "org.opends.server.core.AccessControlConfigManager";

  /** The single application-wide instance. */
  private static AccessControlConfigManager instance;

  /** The active access control implementation. */
  private AtomicReference<AccessControlHandler<?>> accessControlHandler;

  /** The current configuration. */
  private AccessControlHandlerCfg currentConfiguration;

  private ServerContext serverContext;

  /** Creates a new instance of this access control configuration manager. */
  private AccessControlConfigManager()
  {
    this.accessControlHandler = new AtomicReference<AccessControlHandler<?>>(new DefaultAccessControlHandler());
    this.currentConfiguration = null;
  }

  /**
   * Get the single application-wide access control manager instance.
   *
   * @return The access control manager.
   */
  public static AccessControlConfigManager getInstance()
  {
    if (instance == null)
    {
      instance = new AccessControlConfigManager();
    }

    return instance;
  }

  /**
   * Determine if access control is enabled according to the current
   * configuration.
   *
   * @return  {@code true} if access control is enabled, {@code false}
   *          otherwise.
   */
  public boolean isAccessControlEnabled()
  {
    return currentConfiguration.isEnabled();
  }

  /**
   * Get the active access control handler.
   * <p>
   * When access control is disabled, this method returns a default access
   * control implementation which permits all operations.
   *
   * @return   The active access control handler (never {@code null}).
   */
  public AccessControlHandler<?> getAccessControlHandler()
  {
    return accessControlHandler.get();
  }

  /**
   * Initializes the access control sub-system. This should only be called at
   * Directory Server startup. If an error occurs then an exception will be
   * thrown and the Directory Server will fail to start (this prevents
   * accidental exposure of user data due to misconfiguration).
   *
   * @param serverContext
   *          The server context.
   * @throws ConfigException
   *           If an access control configuration error is detected.
   * @throws InitializationException
   *           If a problem occurs while initializing the access control handler
   *           that is not related to the Directory Server configuration.
   */
  public void initializeAccessControl(ServerContext serverContext)
         throws ConfigException, InitializationException
  {
    this.serverContext = serverContext;

    // Don't register as an add and delete listener with the root configuration
    // as we can have only one object at a given time.

    // Initialize the current Access control.
    AccessControlHandlerCfg accessControlConfiguration = serverContext.getRootConfig().getAccessControlHandler();

    // We have a valid usable entry, so register a change listener in
    // order to handle configuration changes.
    accessControlConfiguration.addChangeListener(this);

    //This makes TestCaseUtils.reStartServer happy.
    currentConfiguration=null;

    // The configuration looks valid, so install it.
    updateConfiguration(accessControlConfiguration);
  }

  /**
   * Updates the access control configuration based on the contents of a
   * valid configuration entry.
   *
   * @param  newConfiguration  The new configuration object.
   *
   * @throws  ConfigException If the access control configuration is invalid.
   *
   * @throws  InitializationException  If the access control handler provider
   *                                   could not be instantiated.
   */

  private void updateConfiguration(AccessControlHandlerCfg newConfiguration)
          throws ConfigException, InitializationException
  {
    String newHandlerClass = null;
    boolean enabledOld = false, enabledNew = newConfiguration.isEnabled();

    if (currentConfiguration == null)
    {
      // Initialization phase.
      if (enabledNew)
      {
        newHandlerClass = newConfiguration.getJavaClass();
      }
      else
      {
        newHandlerClass = DefaultAccessControlHandler.class.getName();
      }
      //Get a new handler, initialize it and make it the current handler.
      accessControlHandler.getAndSet(getHandler(newHandlerClass,
              newConfiguration, true, false));
    } else {
      enabledOld = currentConfiguration.isEnabled();
      if(enabledNew) {
        //Access control is either being enabled or a attribute in the
        //configuration has changed such as class name or a global ACI.
        newHandlerClass = newConfiguration.getJavaClass();
        String oldHandlerClass = currentConfiguration.getJavaClass();
        //Check if moving from not enabled to enabled state.
        if(!enabledOld) {
           AccessControlHandler<?> oldHandler =
                   accessControlHandler.getAndSet(getHandler(newHandlerClass,
                                                  newConfiguration, true,
                                                  true));
           oldHandler.finalizeAccessControlHandler();
        } else {
          //Check if the class name is being changed.
          if(!newHandlerClass.equals(oldHandlerClass)) {
            AccessControlHandler<?> oldHandler =
                accessControlHandler.getAndSet(getHandler(newHandlerClass, newConfiguration, true, true));
            oldHandler.finalizeAccessControlHandler();
          } else {
            //Some other attribute has changed, try to get a new non-initialized
            //handler, but keep the old handler.
            getHandler(newHandlerClass,newConfiguration, false, false);
          }
        }
      } else if (enabledOld && !enabledNew) {
        //Access control has been disabled, switch to the default handler and
        //finalize the old handler.
        newHandlerClass = DefaultAccessControlHandler.class.getName();
        AccessControlHandler<?> oldHandler =
            accessControlHandler.getAndSet(getHandler(newHandlerClass, newConfiguration, false, true));
        oldHandler.finalizeAccessControlHandler();
      }
    }
    // Switch in the local configuration.
    currentConfiguration = newConfiguration;
  }

  /**
   * Instantiates a new Access Control Handler using the specified class name,
   * configuration.
   *
   * @param handlerClassName The name of the handler to instantiate.
   * @param config The configuration to use when instantiating a new handler.
   * @param initHandler <code>True</code> if the new handler should be
   *                    initialized.
   * @param logMessage <code>True</code> if an error message should be logged
   *                                     and an alert should be sent.
   * @return The newly instantiated handler.
   *
   * @throws InitializationException  If an error occurs instantiating the
   *                                  the new handler.
   */
  AccessControlHandler<? extends AccessControlHandlerCfg>
  getHandler(String handlerClassName, AccessControlHandlerCfg config,
             boolean initHandler, boolean logMessage)
          throws InitializationException {
    AccessControlHandler<? extends AccessControlHandlerCfg> newHandler;
    try {
      if(handlerClassName.equals(DefaultAccessControlHandler.class.getName())) {
        newHandler = new DefaultAccessControlHandler();
        newHandler.initializeAccessControlHandler(null);
        if(logMessage) {
          LocalizableMessage message = WARN_CONFIG_AUTHZ_DISABLED.get();
          logger.warn(message);
          if (currentConfiguration != null) {
            DirectoryServer.sendAlertNotification(this,
                    ALERT_TYPE_ACCESS_CONTROL_DISABLED, message);
          }
        }
      } else {
        newHandler = loadHandler(handlerClassName, config, initHandler);
        if(logMessage) {
          LocalizableMessage message = NOTE_CONFIG_AUTHZ_ENABLED.get(handlerClassName);
          logger.info(message);
          if (currentConfiguration != null) {
            DirectoryServer.sendAlertNotification(this,
                    ALERT_TYPE_ACCESS_CONTROL_ENABLED, message);
          }
        }
      }
    } catch (Exception e) {
      logger.traceException(e);
      LocalizableMessage message = ERR_CONFIG_AUTHZ_UNABLE_TO_INSTANTIATE_HANDLER.
              get(handlerClassName, config.dn(), stackTraceToSingleLineString(e));
      throw new InitializationException(message, e);
    }
    return newHandler;
  }

  @Override
  public boolean isConfigurationChangeAcceptable(
                      AccessControlHandlerCfg configuration,
                      List<LocalizableMessage> unacceptableReasons)
  {
    try
    {
      // If the access control handler is disabled, we don't care about the
      // configuration.  If it is enabled, then all we care about is whether we
      // can load the access control handler class.
      if (configuration.isEnabled())
      {
        loadHandler(configuration.getJavaClass(), configuration, false);
      }
    }
    catch (InitializationException e)
    {
      unacceptableReasons.add(e.getMessageObject());
      return false;
    }

    return true;
  }

  @Override
  public ConfigChangeResult applyConfigurationChange(
                                 AccessControlHandlerCfg configuration)
  {
    final ConfigChangeResult ccr = new ConfigChangeResult();

    try
    {
      // Attempt to install the new configuration.
      updateConfiguration(configuration);
    }
    catch (ConfigException e)
    {
      ccr.addMessage(e.getMessageObject());
      ccr.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
    }
    catch (InitializationException e)
    {
      ccr.addMessage(e.getMessageObject());
      ccr.setResultCode(DirectoryServer.getCoreConfigManager().getServerErrorResultCode());
    }

    return ccr;
  }

  @Override
  public DN getComponentEntryDN()
  {
    return currentConfiguration.dn();
  }

  @Override
  public String getClassName()
  {
    return CLASS_NAME;
  }

  @Override
  public LinkedHashMap<String,String> getAlerts()
  {
    LinkedHashMap<String,String> alerts = new LinkedHashMap<>();

    alerts.put(ALERT_TYPE_ACCESS_CONTROL_DISABLED,
               ALERT_DESCRIPTION_ACCESS_CONTROL_DISABLED);
    alerts.put(ALERT_TYPE_ACCESS_CONTROL_ENABLED,
               ALERT_DESCRIPTION_ACCESS_CONTROL_ENABLED);

    return alerts;
  }

  /**
   * Loads the specified class, instantiates it as a AccessControlHandler, and
   * optionally initializes that instance.
   *
   * @param  className      The fully-qualified name of the Access Control
   *                        provider class to load, instantiate, and initialize.
   * @param  configuration  The configuration to use to initialize the
   *                        Access Control Handler.  It must not be
   *                        {@code null}.
   * @param  initialize     Indicates whether the access control handler
   *                        instance should be initialized.
   *
   * @return  The possibly initialized Access Control Handler.
   *
   * @throws  InitializationException  If a problem occurred while attempting to
   *                                   initialize the Access Control Handler.
   */
  private <T extends AccessControlHandlerCfg> AccessControlHandler<T>
               loadHandler(String className,
                           T configuration,
                           boolean initialize)
          throws InitializationException
  {
    try
    {
      AccessControlHandlerCfgDefn definition =
           AccessControlHandlerCfgDefn.getInstance();
      ClassPropertyDefinition propertyDefinition =
           definition.getJavaClassPropertyDefinition();
      Class<? extends AccessControlHandler> providerClass =
           propertyDefinition.loadClass(className, AccessControlHandler.class);
      AccessControlHandler<T> provider = providerClass.newInstance();

      if (configuration != null)
      {
        if(initialize) {
          provider.initializeAccessControlHandler(configuration);
        }
      }
      else
      {
        List<LocalizableMessage> unacceptableReasons = new ArrayList<>();
        if (!provider.isConfigurationAcceptable(configuration, unacceptableReasons))
        {
          String reasons = Utils.joinAsString(".  ", unacceptableReasons);
          // Bug: we are in a section where configuration is null
          throw new InitializationException(ERR_CONFIG_AUTHZ_CONFIG_NOT_ACCEPTABLE.get(
                  null /* WAS: configuration.dn() */, reasons));
        }
      }

      return provider;
    }
    catch (Exception e)
    {
      LocalizableMessage message = ERR_CONFIG_AUTHZ_UNABLE_TO_INSTANTIATE_HANDLER.
          get(className, configuration.dn(), stackTraceToSingleLineString(e));
      throw new InitializationException(message, e);
    }
  }
}
